/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { CMError, ErrorCode } from '../api';

import { joinBuffers } from "../utils/buffer_utils";
import { decodeCharset } from '../utils/encodings';
import { getLogger } from "../utils/log";

const logger = getLogger("msg");

/*
 * 二进制读取的接口，所有的数字都是按照小端序处理。
 * 只读，不修改
 */
export interface IBufferView {
  getUint8(): number;
  getUint16(): number;
  getUint16Array(n: number): number[];
  getUint32(): number;
  getBigUint64(): bigint;
  getBytes(n: number): Uint8Array;
  skip(n: number): void;
  seek(n: number): void;
  /*
   * 让游标回到初始位置 + offset 的位置
   */
  reset(offset?: number);
  offset(): number;
  /*
   * 复制一个对象
   * 如果offset是undefined，则保持当前位置指针
   * 否则指针定位到offset的位置
   */
  duplicate(offset?: number): IBufferView;
  end(): boolean;
};

export class BufferView implements IBufferView {
  _buffer: ArrayBuffer;
  _view: DataView;
  _offset: number = 0;
  _initOffset: number = 0;
  constructor(buffer: ArrayBuffer, offset?: number) {
    this._buffer = buffer;
    this._view = new DataView(buffer, 0);
    if (offset) {
      this._offset = offset;
      this._initOffset = offset;
    }
  }

  duplicate(offset?: number): IBufferView {
    return new BufferView(this._buffer, offset === undefined ? this._offset : offset);
  }

  getUint8(): number {
    return this._view.getUint8(this._offset++);
  }

  getUint16(): number {
    const v = this._view.getUint16(this._offset, true);
    this._offset += 2;
    return v;
  }
  getUint16Array(n: number): number[] {
    const arr: number[] = [];
    for (let i = 0; i < n; i++) {
      arr.push(this.getUint16());
    }
    return arr;
  }
  getUint32(): number {
    const v = this._view.getUint32(this._offset, true);
    this._offset += 4;
    return v;
  }
  getBigUint64(): bigint {
    const v = this._view.getBigUint64(this._offset, true);
    this._offset += 8;
    return v;
  }
  getBytes(n: number): Uint8Array {
    const bytes = new Uint8Array(this._view.buffer, this._offset, n);
    this._offset += n;
    return bytes;
  }

  skip(n: number): void {
    this._offset += n;
  }
  seek(n: number): void {
    this._offset = n;
  }
  reset(n?: number): void {
    this._offset = this._initOffset;
    if (n) {
      this._offset += n;
    }
  }
  offset(): number {
    return this._offset;
  }
  end(): boolean {
    return this._offset >= this._buffer.byteLength;
  }
}

enum NextSector {
  DIFAT = 0xfffffffc,
  FATSect = 0xfffffffd,
  EndOfChain = 0xfffffffe,
  FreeSector = 0xffffffff,
}

export enum DirectoryObjectType {
  Unknown = 0,
  Storage = 1,
  Stream = 2,
  RootStorage = 5,
}

export interface DirectoryEntry {
  name: string;
  objectType: DirectoryObjectType;
  colorFlag: number;
  leftSiblingId: number;
  rightSiblingId: number;
  childId: number;
  clsId: Uint8Array;
  stateBits: Uint8Array;
  creationTime: Uint8Array;
  modifiedTime: Uint8Array;
  startSectorLocation: number;
  streamSize: number;
  isStorage(): boolean;
};

export type DirectoryEntryTreeItem = {
  name: string;
  type: number;
  children: DirectoryEntryTreeItem[];
  startSectorLocation: number;
  streamSize: number;
};

export class CFBReader {
  _ds: BufferView;
  _miniFat: number[] = [];
  _fat: number[] = [];
  _miniStreamStartSector: number = 0;
  _sectorShift: number = 0;
  _miniStreamCutoffSize: number = 4096;
  _directoryEntries: DirectoryEntry[] = [];

  constructor(buffer: ArrayBuffer) {
    this._ds = new BufferView(buffer);
    logger.log("MsgReader", buffer);
  }

  getDirectoryEntries(): DirectoryEntry[] {
    return this._directoryEntries;
  }

  parse(): DirectoryEntry[] {
    logger.log("getHeader", this.isValid());
    const ds = this._ds;
    ds.seek(24);
    const minorVersion = ds.getUint16();
    const majorVersion = ds.getUint16();
    const byteOrder = ds.getUint16(); // must be 0xfffe
    // 如果majorVersion是3，必须是0x09，如果majorVersion是4，必须是0x0c
    // 0x09 512字节扇区
    // 0x0c 4096字节扇区
    const sectorShift = ds.getUint16() == 0x09 ? 512 : 4096;
    this._sectorShift = sectorShift;
    const miniSectorShift = ds.getUint16(); // 必须是0x06
    logger.log(`
    majorVersion: ${majorVersion}
    minorVersion: ${minorVersion}
    byteOrder: ${byteOrder}
    sectorShift: ${sectorShift}
    miniSectorShift: ${miniSectorShift}`);
    const reserved = ds.getBytes(6);
    const directorySectorCount = ds.getUint32();
    const fatSectorCount = ds.getUint32();
    logger.log("offset", ds.offset().toString(16));
    const firstDirectorySectorLocation = ds.getUint32();
    const transactionSignatureNumber = ds.getUint32();
    const miniStreamCutoffSize = ds.getUint32(); // must be 4096
    const firstMiniFATSectorLocation = ds.getUint32();
    const miniFATSectorCount = ds.getUint32();
    const firstDIFATSectorLocation = ds.getUint32();
    const difatSectorCount = ds.getUint32();
    const difat = [];
    logger.log(`
    reserved: ${reserved}
    directorySectorCount: ${directorySectorCount}
    fatSectorCount: ${fatSectorCount}
    firstDirectorySectorLocation: ${firstDirectorySectorLocation}
    transactionSignatureNumber: ${transactionSignatureNumber}
    miniStreamCutoffSize: ${miniStreamCutoffSize}
    firstMiniFATSectorLocation: ${firstMiniFATSectorLocation}
    miniFATSectorCount: ${miniFATSectorCount}
    firstDIFATSectorLocation: ${firstDIFATSectorLocation}
    difatSectorCount: ${difatSectorCount}
    ${ds.offset().toString(16)}
    `);

    // 头部还可以最多存放109个DIFAT项
    for (let i = 0; i < 109; i++) {
      const v = ds.getUint32();
      if (v != 0xffffffff) {
        difat.push(v);
      }
    }
    if (difatSectorCount > 0) {
      let difatIndex = firstDIFATSectorLocation;
      const entryNumberPerSector = sectorShift / 4;
      let n = difatSectorCount;
      while (difatIndex != 0xfffffffe && n-- > 0) {
        ds.seek((difatIndex + 1) * sectorShift);
        for (let i = 0; i < entryNumberPerSector - 1; i++) {
          const v = ds.getUint32();
          if (v != 0xffffffff) {
            difat.push(v);
          }
        }
        difatIndex = ds.getUint32();
      }
    }
    // logger.log("difat", difat);

    // 构造FAT表
    const entryNumberPerSector = sectorShift / 4;
    let fatSect: number[] = [];
    for (const df of difat) {
      ds.seek((df + 1) * sectorShift);
      for (let i = 0; i < entryNumberPerSector; i++) {
        fatSect.push(ds.getUint32());
      }
    }
    // logger.log("difat", fatSect.map(v => v.toString(10)).join(" "));
    this._fat = fatSect;
    logger.info("fat", this._fat.length);

    // 构造miniFAT表
    let miniFatIndex: number = firstMiniFATSectorLocation;
    let miniFatSect: number[] = [];
    for (let n = 0; n < miniFATSectorCount; n++) {
      ds.seek((miniFatIndex + 1) * sectorShift);
      for (let i = 0; i < entryNumberPerSector; i++) {
        miniFatSect.push(ds.getUint32());
      }
      /*
      logger.log(
        "miniFatSect",
        miniFatIndex,
        miniFatSect.map(v => v.toString(10)).join(" ")
      );
      */
      miniFatIndex = fatSect[miniFatIndex];
    }
    this._miniFat = miniFatSect;
    logger.info("mini fat", this._miniFat.length);

    // 读取directory
    let sectorIndex = firstDirectorySectorLocation;
    let nodes: DirectoryEntry[] = [];
    const directoryEntrySize = 128;
    while (true) {
      // logger.log("sectorIndex", sectorIndex);
      let directoryOffset = (sectorIndex + 1) * sectorShift;
      let sz = 0;
      while (sz < sectorShift) {
        const node = this.readDirectory(directoryOffset + sz);
        if (node) {
          nodes.push(node);
        }
        sz += directoryEntrySize;
      }
      logger.info('nodes', nodes.length);
      // console.log(arr);
      // nodes = nodes.concat(arr);
      const nextSector = fatSect[sectorIndex];
      if (nextSector == NextSector.EndOfChain) {
        break;
      }
      sectorIndex = nextSector;
    }
    this._miniStreamStartSector = nodes[0].startSectorLocation;
    this._directoryEntries = nodes;
    logger.info("nodes", nodes);
    // logger.log("nodes", nodes, nodes.length);
    // const root = this.buildDirectoryTree(nodes);
    // logger.log("root", JSON.stringify(root, null, 2));
    return nodes;
  }

  /*
   * 读取一个目录项的数据，entry.objectType必须是DirectoryObjectType.Storage
   * 在cfb格式里面，storage是类似于文件夹的概念
   */
  readStorage(entry: DirectoryEntry): DirectoryEntry[] {
    if (entry.objectType != DirectoryObjectType.Storage && entry.objectType != DirectoryObjectType.RootStorage) {
      throw new CMError("not a storage", ErrorCode.PARAMETER_ERROR);
    }
    if (entry.childId == 0xffffffff) {
      return [];
    }
    const cidList = [entry.childId];
    const children: DirectoryEntry[] = [];
    while (cidList.length > 0) {
      const cid = cidList.shift()!;
      const entry = this._directoryEntries[cid];
      children.push(entry);
      if (entry.leftSiblingId != 0xffffffff) {
        cidList.push(entry.leftSiblingId);
      }
      if (entry.rightSiblingId != 0xffffffff) {
        cidList.push(entry.rightSiblingId);
      }
    }
    return children;
  }

  getRootEntry(): DirectoryEntry {
    return this._directoryEntries[0]
  }

  /*
   * 读取一个目录项的数据，entry.objectType必须是DirectoryObjectType.Stream
   * 在cfb格式里面，stream是类似于文件的概念
   */
  readStream(entry: DirectoryEntry): Uint8Array {
    if (entry.objectType != DirectoryObjectType.Stream) {
      throw new CMError("not a stream", ErrorCode.PARAMETER_ERROR);
    }
    const data: Uint8Array[] = [];
    if (entry.streamSize >= this._miniStreamCutoffSize) {
      let sector = entry.startSectorLocation;
      while (sector != NextSector.EndOfChain) {
        data.push(this.getSector(sector));
        sector = this._fat[sector];
      }
    } else {
      let block = entry.startSectorLocation;
      while (block != NextSector.EndOfChain) {
        // logger.debug("block", block);
        data.push(this.getMinStreamSector(block));
        block = this._miniFat[block];
      }
    }
    if (data.length) {
      const totalSize = data[0].byteLength * data.length;
      const delta = totalSize - entry.streamSize;
      const length = data[0].byteLength - delta;
      data[data.length - 1] = data[data.length - 1].subarray(0, length);
    }
    return joinBuffers(data);
  }

  getMinStreamSector(sector: number): Uint8Array {
    const miniStreamOffset = sector * 64;
    let n = Math.floor(miniStreamOffset / this._sectorShift);
    let o = miniStreamOffset % this._sectorShift;
    let startSector = this._miniStreamStartSector;
    while (n > 0) {
      startSector = this._fat[startSector];
      n -= 1;
    }
    const fileOffset = (startSector + 1) * this._sectorShift + o;
    // logger.debug("getSector", sector, startSector, fileOffset.toString(16));
    this._ds.seek(fileOffset);
    return this._ds.getBytes(64);
  }

  getSector(sector: number): Uint8Array {
    const offset = (sector + 1) * this._sectorShift;
    this._ds.seek(offset);
    return this._ds.getBytes(this._sectorShift);
  }

  // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/60fe8611-66c3-496b-b70d-a504c94c9ace
  readDirectory(offset: number): DirectoryEntry {
    return new DirectoryEntryImpl(this._ds.duplicate(offset));
  }

  isValid(): boolean {
    // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/05060311-bfce-4b12-874d-71fd4ce63aea
    // const fileHeader = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
    const valid = this._ds.getUint32() == 0xe011cfd0 && this._ds.getUint32() == 0xe11ab1a1;
    return valid;
  }
}

// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/60fe8611-66c3-496b-b70d-a504c94c9ace
class DirectoryEntryImpl implements DirectoryEntry {
  _view: IBufferView;
  constructor(view: IBufferView) {
    this._view = view;
  }

  get name(): string {
    const view = this._view;
    view.reset(0x40);
    const directoryEntryNameLen = view.getUint16();
    view.reset();
    const buff = view.getBytes(directoryEntryNameLen);
    const value = decodeCharset(buff, 'utf-16le');
    return value;
  }

  isStorage(): boolean {
    const ot = this.objectType;
    return ot == DirectoryObjectType.Storage || ot == DirectoryObjectType.RootStorage;
  }
  get objectType(): DirectoryObjectType {
    this._view.reset(0x42);
    return this._view.getUint8();
  }
  get colorFlag(): number {
    this._view.reset(0x43);
    return this._view.getUint8();
  }
  get leftSiblingId(): number {
    this._view.reset(0x44);
    return this._view.getUint32();
  }
  get rightSiblingId(): number {
    this._view.reset(0x48);
    return this._view.getUint32();
  }
  get childId(): number {
    this._view.reset(0x4c);
    return this._view.getUint32();
  }
  get clsId(): Uint8Array {
    this._view.reset(0x50);
    return this._view.getBytes(16);
  }
  get stateBits(): Uint8Array {
    this._view.reset(0x60);
    return this._view.getBytes(4);
  }
  get creationTime(): Uint8Array {
    this._view.reset(0x64);
    return this._view.getBytes(8);
  }
  get modifiedTime(): Uint8Array {
    this._view.reset(0x6c);
    return this._view.getBytes(8);
  }
  get startSectorLocation(): number {
    this._view.reset(0x74);
    return this._view.getUint32();
  }
  get streamSize(): number {
    this._view.reset(0x78);
    // 实际是8字节，但是一般文件没那么大。小端序存储，所以只需要拿低地址的32位即可
    // 对于版本3的，也只能拿低地址的32位数据
    return this._view.getUint32();
  }
};
